package drds.server.config.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.ObjectStreamConstants;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ReflectionProvider {

	private transient Map<Class<?>, byte[]> serializedDataCache = Collections.synchronizedMap(new HashMap<Class<?>, byte[]>());
	private transient FieldDictionary fieldDictionary = new FieldDictionary();

	public Object newInstance(Class<?> type) {
		try {
			Constructor<?>[] declaredConstructors = type.getDeclaredConstructors();
			for (int i = 0; i < declaredConstructors.length; i++) {
				if (declaredConstructors[i].getParameterTypes().length == 0) {
					if (!Modifier.isPublic(declaredConstructors[i].getModifiers())) {
						declaredConstructors[i].setAccessible(true);
					}
					return declaredConstructors[i].newInstance(new Object[0]);
				}
			}
			if (Serializable.class.isAssignableFrom(type)) {
				return instantiateUsingSerialization(type);
			} else {
				throw new ObjectAccessException("Cannot construct " + type.getName() + " as it does not have a no-args constructor");
			}
		} catch (InstantiationException e) {
			throw new ObjectAccessException("Cannot construct " + type.getName(), e);
		} catch (IllegalAccessException e) {
			throw new ObjectAccessException("Cannot construct " + type.getName(), e);
		} catch (InvocationTargetException e) {
			if (e.getTargetException() instanceof RuntimeException) {
				throw (RuntimeException) e.getTargetException();
			} else if (e.getTargetException() instanceof Error) {
				throw (Error) e.getTargetException();
			} else {
				throw new ObjectAccessException("Constructor for " + type.getName() + " threw an exception", e.getTargetException());
			}
		}
	}

	public void visitSerializableFields(Object object, Visitor visitor) {
		for (Iterator<Field> iterator = fieldDictionary.serializableFieldsFor(object.getClass()); iterator.hasNext();) {
			Field field = iterator.next();
			if (!fieldModifiersSupported(field)) {
				continue;
			}
			validateFieldAccess(field);
			try {
				Object value = field.get(object);
				visitor.visit(field.getName(), field.getType(), field.getDeclaringClass(), value);
			} catch (IllegalArgumentException e) {
				throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
			} catch (IllegalAccessException e) {
				throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
			}
		}
	}

	public void writeField(Object object, String fieldName, Object value, Class<?> definedIn) {
		Field field = fieldDictionary.field(object.getClass(), fieldName, definedIn);
		validateFieldAccess(field);
		try {
			field.set(object, value);
		} catch (IllegalArgumentException e) {
			throw new ObjectAccessException("Could not set field " + field.getName() + "@" + object.getClass(), e);
		} catch (IllegalAccessException e) {
			throw new ObjectAccessException("Could not set field " + field.getName() + "@" + object.getClass(), e);
		}
	}

	public void invokeMethod(Object object, String methodName, Object value, Class<?> definedIn) {
		try {
			Method method = object.getClass().getMethod(methodName, new Class[] { value.getClass() });
			method.invoke(object, new Object[] { value });
		} catch (Exception e) {
			throw new ObjectAccessException("Could not invoke " + object.getClass() + "." + methodName, e);
		}
	}

	public Class<?> getFieldType(Object object, String fieldName, Class<?> definedIn) {
		return fieldDictionary.field(object.getClass(), fieldName, definedIn).getType();
	}

	public boolean fieldDefinedInClass(String fieldName, Class<?> type) {
		try {
			Field field = fieldDictionary.field(type, fieldName, null);
			return fieldModifiersSupported(field);
		} catch (ObjectAccessException e) {
			return false;
		}
	}

	public Field getField(Class<?> definedIn, String fieldName) {
		return fieldDictionary.field(definedIn, fieldName, null);
	}

	private Object instantiateUsingSerialization(Class<?> type) {
		try {
			byte[] bytes;
			if (serializedDataCache.containsKey(type)) {
				bytes = serializedDataCache.get(type);
			} else {
				ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
				DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
				dataOutputStream.writeShort(ObjectStreamConstants.STREAM_MAGIC);
				dataOutputStream.writeShort(ObjectStreamConstants.STREAM_VERSION);
				dataOutputStream.writeByte(ObjectStreamConstants.TC_OBJECT);
				dataOutputStream.writeByte(ObjectStreamConstants.TC_CLASSDESC);
				dataOutputStream.writeUTF(type.getName());
				dataOutputStream.writeLong(ObjectStreamClass.lookup(type).getSerialVersionUID());
				dataOutputStream.writeByte(2); // classDescFlags (2 =
												// Serializable)
				dataOutputStream.writeShort(0); // field count
				dataOutputStream.writeByte(ObjectStreamConstants.TC_ENDBLOCKDATA);
				dataOutputStream.writeByte(ObjectStreamConstants.TC_NULL);
				bytes = byteArrayOutputStream.toByteArray();
				serializedDataCache.put(type, bytes);
			}

			ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
			return objectInputStream.readObject();
		} catch (IOException e) {
			throw new ObjectAccessException("Cannot create " + type.getName() + " by JDK serialization", e);
		} catch (ClassNotFoundException e) {
			throw new ObjectAccessException("Cannot find class " + e.getMessage());
		}
	}

	private boolean fieldModifiersSupported(Field field) {
		return !(Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers()));
	}

	private void validateFieldAccess(Field field) {
		if (Modifier.isFinal(field.getModifiers())) {
			field.setAccessible(true);
		}
	}

	private Object readResolve() {
		serializedDataCache = Collections.synchronizedMap(new HashMap<Class<?>, byte[]>());
		fieldDictionary = new FieldDictionary();
		return this;
	}

}